Skip to content

Add Node.js / Bun bindings via napi-rs - #15

Merged
artiz merged 4 commits into
masterfrom
claude/nodejs-bun-typescript-bindings-8pk2wk
Jul 1, 2026
Merged

Add Node.js / Bun bindings via napi-rs#15
artiz merged 4 commits into
masterfrom
claude/nodejs-bun-typescript-bindings-8pk2wk

Conversation

@artiz

@artiz artiz commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Adds native TypeScript bindings for Fleischwolf that run in both Node.js and Bun via N-API. The bindings expose the full converter API with feature parity to the Rust library.

Summary

This introduces crates/fleischwolf-node, a new napi-rs crate that compiles to a native .node addon. The addon loads in both Node.js and Bun (which implements N-API) without requiring a rebuild between runtimes. The TypeScript surface mirrors the Rust DocumentConverter, supporting:

  • Sync and async conversion (convert, convertFile, convertAsync, convertFileAsync)
  • Streaming Markdown via streamFileMarkdown async generator (chunks in document order)
  • Output formats: Markdown (default) or docling-core JSON
  • Image handling: placeholder, embedded (base64 data URIs), or referenced (separate files)
  • Converter options: strict mode, external image fetching, allowed-format restriction
  • Reusable converter class (DocumentConverter) for batch processing

Key Changes

  • crates/fleischwolf-node/src/lib.rs (627 lines): Core N-API binding implementation

    • Module-level one-shot functions (convert_file, convert, convert_file_async, convert_async)
    • DocumentConverter class with sync/async methods and streaming support
    • Internal ConvertConfig and RawResult types for thread-safe off-loop execution
    • Format parsing, output kind resolution, and error handling
    • Streaming via convert_file_streaming with threadsafe callbacks
  • crates/fleischwolf-node/index.js: Public entry point

    • Re-exports native binding functions
    • Implements streamFileMarkdown as an async generator wrapping the native callback-based streaming API
  • crates/fleischwolf-node/index.d.ts: TypeScript type definitions for the public API

  • crates/fleischwolf-node/package.json: npm package metadata

    • Targets x86_64/aarch64 Linux, macOS, and Windows
    • Configured for both Node.js (14+) and Bun
  • crates/fleischwolf-node/Cargo.toml: Rust crate configuration

    • cdylib library type for N-API addon
    • Dependencies: fleischwolf, napi, napi-derive, napi-build
  • crates/fleischwolf-node/build.rs: Build script for N-API symbol resolution

  • Documentation and examples:

    • README.md: Comprehensive guide with API reference, quick start, and examples
    • examples/node-basic.mjs: Node.js ESM example (file conversion, bytes, JSON, reusable converter)
    • examples/bun-basic.ts: Bun TypeScript example (async, streaming, embedded images)
    • test/smoke.mjs: Smoke test covering all major code paths
  • Root Cargo.toml: Added crates/fleischwolf-node to workspace members

Notable Implementation Details

  • Thread safety: Conversion runs on libuv thread pool for async paths; ConvertConfig and RawResult are Send-safe (no napi types) to cross thread boundaries
  • Streaming: Uses ThreadsafeFunction with ErrorStrategy::CalleeHandled to marshal chunks from a background thread back to the JS event loop
  • Format inference: Supports both format ids ("pdf", "md") and extensions (".html", ".docx")
  • Image modes: Placeholder (no images), embedded (base64 data URIs), and referenced (separate files with paths)
  • Error handling: Converts Rust errors to napi Error with appropriate status codes
  • Async generator: streamFileMarkdown bridges the native callback-based API into a clean async-iterable interface

https://claude.ai/code/session_01L7XwjsWSHWjrh39JxMLuB3

claude added 4 commits July 1, 2026 08:25
Add a napi-rs binding crate exposing the DocumentConverter to Node.js and
Bun via a single native N-API addon (Bun implements N-API, so the same
binary loads in both — no rebuild).

The TypeScript surface mirrors the Rust API:
- convertFile / convert (in-memory bytes) plus Promise-returning *Async
  variants that run the CPU-bound work off the event loop
- Markdown or docling-core JSON output; strict Markdown; placeholder /
  embedded / referenced image modes; allowedFormats; fetchImages
- a reusable DocumentConverter class
- streamFileMarkdown: an async generator over Markdown chunks in document
  order (the streaming win for PDF), wrapping the native callback API
- supportedFormats / formatFromName helpers

Packaging: @napi-rs/cli build, generated + hand-written TypeScript types,
a self-referencing exports map (works from ESM and CommonJS), a smoke test
that passes identically under Node and Bun, and runnable Node + Bun/TS
examples. Generated artifacts (.node, native.js/.d.ts, node_modules) are
gitignored; run `npm install && npm run build`.

The crate joins the workspace (covered by fmt/clippy/test) but is marked
publish=false — it ships to npm, not crates.io.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7XwjsWSHWjrh39JxMLuB3
Add a reusable `Pipeline` class that keeps the ONNX models (layout, OCR,
TableFormer with the KV-cached decoder) loaded across calls — the one-shot
convert functions rebuild the pipeline and reload every model each call.
Handles pdf/image inputs; synchronous, reuse one instance across documents.

Add `installDependencies()` / `checkDependencies()` for the PDF/image assets
that aren't bundled in the addon, mirroring how docling provisions models:
- pdfium (bblanchon, platform-detected) and the PP-OCR model + dictionary are
  downloaded automatically over HTTP(S);
- the layout and TableFormer ONNX (PyTorch->ONNX exports with no public
  prebuilt download) are fetched from a base URL supplied via
  `{ modelsUrl }` or `FLEISCHWOLF_MODELS_URL`, or detected from local
  `DOCLING_*` env paths;
- everything installs under `~/.cache/fleischwolf` (or `$FLEISCHWOLF_HOME` /
  `dir`) and the matching `DOCLING_*` / `PDFIUM_DYNAMIC_LIB_PATH` env vars are
  wired in-process. Idempotent; throws with an actionable message when PDF
  conversion can't be made ready.

Guard every entry point (module functions + DocumentConverter + Pipeline +
streamFileMarkdown): converting a pdf/image/METS input before the deps are
installed now throws (or rejects, for async) an error pointing at
`installDependencies()`. Declarative formats are unaffected.

Docs: package + root README document the model provisioning and warm Pipeline;
add examples/pdf-pipeline.mjs. Smoke test covers the guards and dependency
status (16 checks, passing under Node and Bun).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7XwjsWSHWjrh39JxMLuB3
…lease

Add .github/workflows/npm-publish.yml: on each `v*` release tag (pushed by
scripts/release.sh), build the native addon on a matrix of native runners —
Linux x64 (ubuntu-22.04), Linux arm64 (ubuntu-24.04-arm), macOS x64 (macos-13),
macOS arm64 (macos-14), Windows x64 — then a publish job assembles the main
`fleischwolf` package plus per-platform `fleischwolf-<triple>` packages
(optionalDependencies) via napi-rs and publishes them all to npm. The version
is taken from the tag so it tracks the crate release; already-published
versions are skipped for idempotent re-runs.

Configure the package for the napi-rs v2 multi-platform flow: napi.name +
napi.triples for the five targets, and prepublishOnly / artifacts / version
scripts. The main package ships only the JS loader + types (~50 kB); the
prebuilt .node binaries ride in the platform packages. README documents
`npm install fleischwolf` (prebuilt) alongside build-from-source.

Requires an NPM_TOKEN repository secret with publish rights.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7XwjsWSHWjrh39JxMLuB3
Switch npm-publish.yml from an automatic `v*` tag trigger to
workflow_dispatch-only: pick the release tag to build, and the workflow checks
out that tag (all matrix + publish jobs) and publishes the npm version derived
from it. Keeps npm releases decoupled from the crates.io release that runs on
each master push. An optional `version` input overrides the derived version.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7XwjsWSHWjrh39JxMLuB3
@artiz
artiz merged commit a8e5f5d into master Jul 1, 2026
3 checks passed
@artiz
artiz deleted the claude/nodejs-bun-typescript-bindings-8pk2wk branch July 1, 2026 09:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants